Skip to content

fix(core-ts,mcp-server): RFC-0045 step 7a, held rows for old-shape values and whole-list re-translation - #221

Merged
Madraka merged 2 commits into
mainfrom
claude/rfc45-step7a-a251a6
Nov 13, 2026
Merged

Madraka merged 2 commits into
mainfrom
claude/rfc45-step7a-a251a6

Conversation

@Madraka

@Madraka Madraka commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

RFC-0045 rollout step 7a: the two latent issues to fix before step 7b enables reference and repeater fields. Both types stay refused on every production write path; the shared suites enable them with SiteGraphStoreOptions.fieldTypes.

Closes #215, closes #218.

#215: old-shape values keep their entry_refs rows (core-ts)

  • normalizeWrite → onUnconvertible used to return early when convertingPath(def) was undefined. That covers every top-level conversion from references with no text ↔ reference path: reference (multiple) ↔ repeater, reference single ↔ multiple, and a repeater converting away.
  • So a stored old-shape value that did not convert was not held, and entryRefStatements dropped its rows on any unrelated write.
  • Such failures are now recorded at the old type's field paths: each id-shaped string at <field> from a reference, and each item's id-shaped sub-value at <field>.<sub> from a repeater. heldRefs and entryRefStatements keep them, and every stored failing id is held, as before for conversions from references.
  • The rebuild's reshapedRows and the writer share one helper, references/old-shape.ts (readsOldShape, oldShapePairs), so the rows are the same whichever touched the node last.
  • Test: src/testing-field-operations-old-shape.ts, registered in the shared suite, so it runs on SQLite, Miniflare D1 and DO SQLite. It fails on all three without the fix.
    • Reference → repeater with a malformed stored list ([id, 5], injected with json_set as an import could hold it):
      1. a title edit keeps the rows, and deleting the target is entry_referenced;
      2. a reimport (the rebuild) gives the same rows and the same refusal;
      3. a write after the rebuild gives the same again.
    • Repeater → reference with an item of another shape: the same rows after a write, a rebuild and a write.
    • Target deletes there are field_operation_running for the whole operation, because a conversion into references holds them (R12). So the row checks carry this case.

#218: re-translating a repeater the translation already holds (mcp-server, core-ts)

  1. When the existing translation (the one save_translation writes) holds its own list for a localized repeater, get_translation_source returns the source's whole list for that key under fields, in source order and with every sub-value, instead of item paths. A new wholeLists output names those keys. The logic is in core-ts translationSourceValues (new export); MCP's translationSourceFields takes the existing translation.

  2. Updated text:

    • the translation_shape_mismatch hint in field-hints.ts (send the whole list, as get_translation_source returns it, and no item paths for it);
    • TRANSLATED_FIELDS;
    • get_translation_source's fields and wholeLists descriptions;
    • save_translation's description.

    The README tool table is unchanged after node scripts/mcp-tools-doc.ts: it truncates before the edited part, and --check passes.

  3. Docs:

    • The RFC's "Amended in 5" bullet gets a short Amended in 7a qualifier: the length check applies only when item paths come in the same call, and a whole list sent alone is the translation's own and stored as sent.
    • A new "Amended in 7a" section records both fixes.
    • The new changeset .changeset/rfc45-step7a-fixes.md (core-ts and mcp-server, patch) qualifies the step-5 wording. The step-5 changeset is untouched.
    • Code comments in fields/translation.ts (baseList) and translation-fields.ts say the same.
  4. Tests:

    • mcp-server/test/references.test.ts, "re-translates a stale repeater: list_stale_translations → get_translation_source → save_translation":
      1. the first save goes by item paths;
      2. the source is edited and lists the translation as stale;
      3. get_translation_source returns wholeLists: ["agenda"] and the source's list;
      4. item paths are refused with the new hint;
      5. the handed-over list, translated, saves, and nothing is stale afterwards.
    • core-ts fields-translation.test.ts: unit cases for translationSourceValues, and for a whole list sent alone being stored as sent.

Local checks

  • Single files: core-ts test/store.test.ts:
    • better-sqlite3: all RFC-0045 suites, 111 passed;
    • D1 and DO SQLite, each: §7 suites 51 passed, step-2 integrity 49 passed. They ran one slice at a time, because a full D1 pass here exhausts local ports (EADDRNOTAVAIL).
  • core-ts fields-translation.test.ts: 11 passed. mcp-server references.test.ts: 17 passed.
  • pnpm size:check, typecheck, biome and mcp-tools-doc --check: clean.
  • Full pnpm check: result in the session report.

🤖 Generated with Claude Code

Madraka and others added 2 commits November 13, 2026 18:10
…lues and whole-list re-translation

#215: while a top-level conversion from references runs with no text <->
reference path (reference (multiple) <-> repeater, single <-> multiple, a
repeater converting away), a stored old-shape value that does not convert
now keeps its entry_refs rows through a person's unrelated write.
normalizeWrite holds them at the old type's paths, the same pairs the
rebuild keeps (one helper, references/old-shape.ts oldShapePairs), so the
rows and the target's delete guard no longer depend on whether a write or
a reimport touched the node last. Shared-suite test on SQLite, Miniflare
D1 and DO SQLite.

#218: once a translation holds its own list for a localized repeater,
get_translation_source returns the source's whole list for that key under
fields (source order, every sub-value) instead of item paths, and names it
in a new wholeLists output (core-ts translationSourceValues). The
translation_shape_mismatch hint, TRANSLATED_FIELDS and the tool
descriptions say to send it back whole. The RFC (Amended in 7a) and a new
changeset qualify the step-5 wording: the length check applies only when
item paths come in the same call; a whole list sent alone is stored as
sent. Test for list_stale_translations -> get_translation_source ->
save_translation.

Closes #215, closes #218.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Held entry_refs rows follow their holder's status: when a write holds
  pairs, the same batch updates their source_status to the entry's new
  status, anchored on its new hash (references/held-status.ts). Covers
  reshapes and text <-> reference held values. Suite cases: publish locks
  the target, unpublish frees it, and a reference -> text held row.
- Rebuild legs are real: rebuildingImport adds a node after the export and
  drops the site's rows first, and asserts the import is not unchanged.
  The four no-op reimports in testing-field-operations-references.ts use
  it too. Rows are compared with source_status.
- translation_shape_mismatch hint covers both refusals (own list, and a
  whole list beside paths of another length).
- get_translation_source's description names wholeLists; the pending
  step-5 changeset states the length rule as it is, and the 7a changeset
  drops its redundant bullet.
- save_translation checks a given sourceRev against whole repeater lists
  too (conflict when the source moved on); optional there. Test added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka merged commit e0f4cf6 into main Nov 13, 2026
8 checks passed
@Madraka
Madraka deleted the claude/rfc45-step7a-a251a6 branch November 13, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant